Skip to content

Conversation

jtpio
Copy link
Member

@jtpio jtpio commented Aug 22, 2025

Fixes #6692
Fixes #1135

To offer a behavior similar to the classic notebook:

notebook-down-pager.mp4

The idea would be to have this behavior by default in Notebook 7.5, with a setting to disable it and have the pager data be displayed in the cell output like in JupyterLab:

notebook-pager-disabled.mp4

Example with xeus-cpp showing a text/html bundle:

notebook-xeus-cpp-pager.mp4
  • Open the pager in the down area
  • Setting to disable this behavior and default to the JupyterLab behavior
  • If disabled, allow opening the "Contextual Help" and click around like in JupyterLab
  • UI test
  • Update snapshots
  • Docs

Copy link
Contributor

Binder 👈 Launch a Binder on branch jtpio/notebook/down-pager

@jtpio jtpio added this to the 7.5.0 milestone Aug 22, 2025
@jtpio
Copy link
Member Author

jtpio commented Aug 28, 2025

So, I think this PR should be ready for a first round of reviews.

As a quick summary, it brings back the behavior of the pager just like the classic notebook. This has been missing since the early Jupyter Notebook 7 releases (released a bit more than two years ago), and there's been some feedback that this was still missing.

I've also added some UI tests to cover some cases, and also added a new page in the docs so that folks can opt out of this new behavior and return to the JupyterLab behavior if they want to.

If folks would like to try it, that would be great, let me know what you think. I'm also happy to iterate on a different approach if you think the current one could be improved.

@jtpio jtpio marked this pull request as ready for review August 28, 2025 16:59
@jtpio jtpio requested a review from Copilot August 29, 2025 12:23
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements a pager widget that displays help documentation in the down area (inspector panel) similar to classic notebook behavior, providing an alternative to JupyterLab's inline output approach. Users can toggle this behavior through settings to maintain compatibility with either classic notebook or JupyterLab workflows.

  • Implements a new pager plugin that intercepts kernel messages with pager payloads and displays them in the inspector panel
  • Adds configuration option to switch between down-area display and inline cell output behavior
  • Updates shell layout to support inspector placement in the down area

Reviewed Changes

Copilot reviewed 9 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/notebook-extension/src/index.ts Core pager plugin implementation with kernel message handling and inspector integration
packages/notebook-extension/schema/pager.json Settings schema for configuring pager behavior
packages/notebook-extension/package.json Added inspector dependency
packages/application/src/shell.ts Enhanced down panel activation to support tab switching
packages/application-extension/schema/shell.json Updated shell configuration to place Inspector in down area by default
ui-tests/test/notebook.spec.ts Added test to verify pager opens in down area with question mark syntax
docs/source/user-documentation.md Added pager documentation reference
docs/source/pager.md Complete user documentation for pager functionality
app/package.json Added inspector extension dependencies and configuration

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copy link
Contributor

@andrii-i andrii-i left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this contribution @jtpio, it's a nice addition to the Notebook UI, especially for users used to Nbclassic. Please find some comments and suggestions below.


// Listen for parent disposal.
panel.disposed.connect(() => {
inspectionHandler.dispose();
Copy link
Contributor

@andrii-i andrii-i Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All inspection handlers would not be properly disposed of because inspectionHandler is a global variable (L901) and each panel.disposed.connect() callback (L967-969) references this global variable, not the specific handler instance created for that panel. So when notebooks are closed only the most recently created handler gets disposed while previous handlers remain in memory with associated resources not being freed.

Test by opening multiple notebooks then closing any other than last created notebook, note that only last handler gets disposed of.

Tracking handlers per panel and ensuring correct disposal of all handlers would fix this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering is this is a real issue in practice.

In Jupyter Notebook there is only one notebook at a time on the page, which actually should never really be disposed. So there should be one inspection handler instantiated on the page.

This code snippet was inspired by the upstream implementation in JupyterLab, but it could be cleaned up or simplified a bit to only handle the Notebook use case.

Comment on lines +1007 to +1008
app.shell.currentChanged?.connect((_, args) => setSource(args.newValue));
void app.restored.then(() => setSource(app.shell.currentWidget));
Copy link
Contributor

@andrii-i andrii-i Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setSource() can be called before inspectionHandler is initialized (L932), for example during app startup, causing undefined access when app.shell events fire before any notebook is opened.

To test, add logging inspectionHandler at L975, launch notebook server without opening any notebooks, refresh the browser page while monitoring the logs, see inspectionHandler is undefined.

While this doesn't break functionality (opening a notebook later initializes the handler), it puts the inspector in an invalid state during startup and could cause issues if other components try to use it during that window.

Adding a null check to setSource() or declaring inspectionHandler as nullable and initializing it to null would fix this.

Comment on lines +872 to +877
(item) => item.source !== 'page'
);
if (content.payload.length === 0) {
// If no other payloads remain, delete the payload array from the content.
delete content.payload;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mutates kernel messages, other extensions / components would receive the modified version missing pager payloads. Based on my research JupyterLab's codebase as of now does not mutate kernel messages in other places. For example, @jupyterlab/outputarea copies the message.

Would it be possible to accomplish the same without mutating the kernel messages? For example watch cell output model changes to detect pager payloads during cell execution and route them to the down area before normal output rendering?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, maybe there is a way to check the outputs directly.

Need to check if this will have any visual side-effect too (for example the output is rendered and then removed).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, @jupyterlab/outputarea copies the message.

I looked a bit more into this. While we could set up handlers to check the output area model, information about the message gets lost when it's added to the output:

const output: nbformat.IOutput = {
  output_type: 'display_data',
  data: (page as any).data as nbformat.IMimeBundle,
  metadata: {}
};
model.add(output);

And there doesn't seem to be any easy way to know if an output is the result of pager information.

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Open the help in a bottom panel support mimebundles
2 participants